React Hook Formでフォームに初期値を非同期で与える
唐突ですがcontrolled componentsで更新系のフォームを扱う場合には下記のようなパターンで描写されることが多いです.
- 初期処理
- DOMの描写が走る
- useEffectなどでデータを非同期にフェッチする
- 値を状態にセットする
- 再描写が走る
さらに唐突ですが, React Hook Form (RHF) はuncontrolled componentsを採用しているためパフォーマンスが優れています.
RFHは値の取得や変更をRef経由で行います. そのため下記のようなコードの場合, TextFieldのラベルがshrinkしません.
<TextField label="california" id="california" name="california" inputRef={register} />
californiaが沈んだままですね. これはいけません.
InputLabelPropsをいじれば見栄えはよくなりますが, 常にラベルが浮いた状態になります. ですが沈まぬことがあるでしょうか.
前置きが長くなりましたが今回はこの問題の対処をうまくできたので執筆していきます.
environment
- RHF v6
- Material UI v4
- React 16.3
concept
コンセプトはかなりシンプルでフォームの描写前に値のフェッチなどを終わらせます. なので流れは下記のようになります.
- 初期処理
- ロード画面を描写させる
- useEffectなどでデータを非同期的にフェッチする
setValue()
を利用して値を入れる- ロード画面の描写を状態を変更してformを描写する
- 再描写が走る
この中でポイントは2つです.
- formの描写をデータの取得が終わった後に行う
- useForm Hooks利用時に
shouldUnregister
をfalseにする
shouldUnregister
をfalseにすることでformがマウントされていない状態でも入力を維持することができます. これを利用してformの描写前に入力を更新する, つまり初期値を入れることができます.
Code
では実際のコードをみていきましょう. TypeScriptで書いていますが実際に動くやつではないので読み替えなりしてください.
Formのコンポーネントだけみていきましょう.
export const CaliforniaForm: React.FC = () => { const { register, handleSubmit, setValue } = useForm<CaliforniaFormInput>({ // defaultがtrueなのでfalseに切り替える shouldUnregister: false, }); const [loading, setLoading] = useState(true); useEffect(() => { const get = async () => { // 非同期的にデータをとってくる const california = await fetchCalifornia('awesome'); // setValueでRHFのformに値を入れる setValue('california', california.data); // setterを呼び出すことで再レンダリングが走る setLoading(false); }; get(); }, []) if (loading) { return <CircularProgress />; } return ( <form> <TextField label="california" id="california" name="california" inputRef={register} /> </form> ); }
useEffectの中で, fetch → setValue → setLoadingの順で呼び出すことができます. これでデータを取得してからマウント前のformに対して初期値を保持して, フォームを最終的にレンダリングするといった実装ができます.
これでcaliforniaは浮き沈みできるようになりました. おめでとうございます.
in the end
Reactが何を契機に再描写が走るかや, 仮想DOMのコンセプトを知っていると動作や実装をどうすべきか考えやすいと感じました.
この記事がお役にたったら幸いです.